<!DOCTYPE html>
<html>
<head>
  <title>Radial Layout</title>
  <meta name="description" content="Radial layout of an arbitrary graph given a start node; selecting a node re-lays out using it as a new root node." />
  <!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
  <meta charset="UTF-8">
  <script src="go.js"></script>
  <link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
  <script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">

  var showCircles = true;  // show a circle behind the nodes in each layer
  var rotateText = true;  // whether to rotate the label with the angle of the node
  var maxLayers = 2;  // how many concentric layers to show
  var layerThickness = 100;  // how thick each ring should be

  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;  // for conciseness in defining templates

    myDiagram =
      $(go.Diagram, "myDiagramDiv", // must be the ID or reference to div
        {
          initialAutoScale: go.Diagram.Uniform,
          initialContentAlignment: go.Spot.Center,
          padding: 10,
          isReadOnly: true,
          maxSelectionCount: 1,
          "animationManager.isEnabled": false
        });

    // shows when hovering over a node
    var commonToolTip =
      $(go.Adornment, "Auto",
        { isShadowed: true },
        $(go.Shape, { fill: "#FFFFCC" }),
        $(go.Panel, "Vertical",
          { margin: 3 },
          $(go.TextBlock,  // bound to node data
            {margin: 4, font: "bold 12pt sans-serif" },
            new go.Binding("text")),
          $(go.TextBlock,  // bound to node data
            new go.Binding("text", "color", function(c) { return "Color: " + c; })),
          $(go.TextBlock,  // bound to Adornment because of call to Binding.ofObject
            new go.Binding("text", "", function(ad) { return "Connections: " + ad.adornedPart.linksConnected.count; }).ofObject())
        )  // end Vertical Panel
      );  // end Adornment

    // define the Node template
    myDiagram.nodeTemplate =
      $(go.Node, "Spot",
        {
          locationSpot: go.Spot.Center,
          locationObjectName: "SHAPE",  // Node.location is the center of the Shape
          selectionAdorned: false,
          selectionChanged: nodeSelectionChanged,
          toolTip: commonToolTip
        },
        $(go.Shape, "Circle",
          {
            name: "SHAPE",
            fill: "lightgray",  // default value, but also data-bound
            stroke: "transparent",
            strokeWidth: 2,
            desiredSize: new go.Size(20, 20),
            portId: ""  // so links will go to the shape, not the whole node
          },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          {
            name: "TEXTBLOCK",
            alignment: go.Spot.Right,
            alignmentFocus: go.Spot.Left
          },
          new go.Binding("text"))
      );

    // this is the root node, at the center of the circular layers
    myDiagram.nodeTemplateMap.add("Root",
      $(go.Node, "Auto",
        {
          locationSpot: go.Spot.Center,
          selectionAdorned: false,
          selectionChanged: nodeSelectionChanged,
          toolTip: commonToolTip
        },
        $(go.Shape, "Circle",
          { fill: "white" }),
        $(go.TextBlock,
          { font: "bold 14pt sans-serif", margin: 10 },
          new go.Binding("text"))
      ));

    // define the Link template
    myDiagram.linkTemplate =
      $(go.Link,
        {
          routing: go.Link.Normal,
          curve: go.Link.Bezier,
          selectionAdorned: false,
          layerName: "Background"
        },
        $(go.Shape,
          { stroke: "black",  // default value, but is data-bound
            strokeWidth: 1 },
          new go.Binding("stroke", "color"))
      );

    generateGraph();
  }

  function generateGraph() {
    var names = [
      "Joshua", "Daniel", "Robert", "Noah", "Anthony",
      "Elizabeth", "Addison", "Alexis", "Ella", "Samantha",
      "Joseph", "Scott", "James", "Ryan", "Benjamin",
      "Walter", "Gabriel", "Christian", "Nathan", "Simon",
      "Isabella", "Emma", "Olivia", "Sophia", "Ava",
      "Emily", "Madison", "Tina", "Elena", "Mia",
      "Jacob", "Ethan", "Michael", "Alexander", "William",
      "Natalie", "Grace", "Lily", "Alyssa", "Ashley",
      "Sarah", "Taylor", "Hannah", "Brianna", "Hailey",
      "Christopher", "Aiden", "Matthew", "David", "Andrew",
      "Kaylee", "Juliana", "Leah", "Anna", "Allison",
      "John", "Samuel", "Tyler", "Dylan", "Jonathan"
    ];

    var nodeDataArray = [];
    for (var i = 0; i < names.length; i++) {
      nodeDataArray.push({ key: i, text: names[i], color: go.Brush.randomColor(128, 240) });
    }

    var linkDataArray = [];
    var num = nodeDataArray.length;
    for (var i = 0; i < num * 2; i++) {
      var a = Math.floor(Math.random() * num);
      var b = Math.floor(Math.random() * num / 4) + 1;
      linkDataArray.push({ from: a, to: (a + b) % num, color: go.Brush.randomColor(0, 127) });
    }

    myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

    var someone = nodeDataArray[Math.floor(Math.random() * nodeDataArray.length)];
    var somenode = myDiagram.findNodeForData(someone);
    myDiagram.select(somenode);
  }

  // called when "Set Max Layers" button is clicked
  function adjustMaxLayers() {
      var newMaxLayers = document.getElementById("maxLayersChanger").value;
      function IsNumeric(val) {
          return Number(parseFloat(val)) == val;
      }
      if (!IsNumeric(newMaxLayers)) alert("Please enter a number");
      else {
          maxLayers = newMaxLayers;
          var root = myDiagram.findNodesByExample({ category: "Root" }).first();
          myDiagram.clearSelection();
          myDiagram.select(root);
      }
  }

  // when a new node is selected, adjust the radial layout around the new node
  function nodeSelectionChanged(node) {
    var diagram = node.diagram;
    if (diagram === null) return;
    if (node.isSelected) {
      // make this Node the root
      node.category = "Root";
      // determine new distances from this new root node
      var results = findDistances(node);
      radialLayout(node, results);
    }
  }

  // returns a Map of Nodes with distance values
  function findDistances(source) {
    var diagram = source.diagram;
    // keep track of distances from the source node
    var distances = new go.Map(go.Node, "number");
    diagram.nodes.each(function(n) {
      distances.add(n, Infinity);
    });
    // the source node starts with distance 0
    distances.add(source, 0);
    // keep track of nodes for we have set a non-Infinity distance,
    // but which we have not yet finished examining
    var seen = new go.Set(go.Node);
    seen.add(source);

    // local function for finding a Node with the smallest distance in a given collection
    function leastNode(coll, distances) {
      var bestdist = Infinity;
      var bestnode = null;
      var it = coll.iterator;
      while (it.next()) {
        var n = it.value;
        var dist = distances.getValue(n);
        if (dist < bestdist) {
          bestdist = dist;
          bestnode = n;
        }
      }
      return bestnode;
    }

    // keep track of nodes we have finished examining;
    // this avoids unnecessary traversals and helps keep the SEEN collection small
    var finished = new go.Set(go.Node);
    while (seen.count > 0) {
      // look at the unfinished node with the shortest distance so far
      var least = leastNode(seen, distances);
      var leastdist = distances.getValue(least);
      // by the end of this loop we will have finished examining this LEAST node
      seen.remove(least);
      finished.add(least);
      // look at all Links connected with this node
      least.linksConnected.each(function(link) {
        var neighbor = link.getOtherNode(least);
        // skip nodes that we have finished
        if (finished.contains(neighbor)) return;
        var neighbordist = distances.getValue(neighbor);
        // assume "distance" along a link is unitary, but could be any non-negative number.
        var dist = leastdist + 1;  //Math.sqrt(least.location.distanceSquaredPoint(neighbor.location));
        if (dist < neighbordist) {
          // if haven't seen that node before, add it to the SEEN collection
          if (neighbordist == Infinity) {
            seen.add(neighbor);
          }
          // record the new best distance so far to that node
          distances.add(neighbor, dist);
        }
      });
    }

    return distances;
  }

  function radialLayout(root, distances) {
    root.diagram.startTransaction("radial layout");
    // sort all results into Arrays of Nodes with the same distance
    var nodes = {};
    var maxlayer = 0;
    var it = distances.iterator;
    while (it.next()) {
      var node = it.key;
      if (node !== root) node.category = "";  // remove "Root" category from all non-root nodes
      node._laid = false;
      var layer = it.value;
      if (layer === Infinity) continue; // Infinity used as init value (set in findDistances())
      if (layer > maxlayer) maxlayer = layer;
      var layernodes = nodes[layer];
      if (layernodes === undefined) {
        layernodes = [];
        nodes[layer] = layernodes;
      }
      layernodes.push(node);
    }

    // optional: add circles in the background
    // need to remove any old ones first
    var gridlayer = root.diagram.findLayer("Grid");
    var circles = new go.Set(go.Part);
    gridlayer.parts.each(function(circle) {
        if (circle.name === "CIRCLE") circles.add(circle);
      });
    circles.each(function(circle) {
        root.diagram.remove(circle);
      });
    // add circles centered at the root
    if (showCircles) {
      var $ = go.GraphObject.make;  // for conciseness in defining templates
      for (var lay = 1; lay <= maxLayers; lay++) {
        var radius = lay * layerThickness;
        var circle =
          $(go.Part,
            { name: "CIRCLE", layerName: "Grid" },
            { locationSpot: go.Spot.Center, location: new go.Point(0, 0) },
            $(go.Shape, "Circle",
              { width: radius * 2, height: radius * 2 },
              { fill: "rgba(200,200,200,0.2)", stroke: null }));
        node.diagram.add(circle);
      }
    }

    // now recursively position nodes (using radlay1()), starting with the root
    root.location = new go.Point(0, 0);
    radlay1(root, 1, 0, 360, distances);
    // finally, hide nodes with distance > maxLayers
    it = distances.iterator;
    while (it.next()) {
      var node = it.key;
      node.visible = (it.value <= maxLayers);
    }
    root.diagram.commitTransaction("radial layout");
  }
  
  // recursively position nodes in a radial layout
  function radlay1(node, layer, angle, sweep, distances) {
    if (layer > maxLayers) return; // no need to position nodes outside of maxLayers
    var nodes = []; // array of all Nodes connected to 'node' in layer 'layer'
    node.findNodesConnected().each(function(n) {
      if (n._laid) return;
      if (distances.getValue(n) === layer) nodes.push(n);
    });
    var found = nodes.length;
    if (found === 0) return;

    var radius = layer * layerThickness;
    var separator = sweep / found; // distance between nodes in their sweep portion
    var start = angle - sweep / 2 + separator / 2;
    // for each node in this layer, place it in its correct layer and position
    for (var i = 0; i < found; i++) {
      var n = nodes[i];
      var a = start + i * separator; // the angle to rotate the node to
      // the point to place the node at -- this corresponds with the layer the node is in
      // all nodes in the same layer are placed at a constant point, then rotated accordingly
      var p = new go.Point(radius, 0);
      p.rotate(a);
      n.location = p;
      n._laid = true;
      // rotates the node's textblock 
      if (rotateText) {
        n.angle = a;
        var label = n.findObject("TEXTBLOCK");
        if (label !== null) {
          label.angle = ((a > 90 && a < 270 || a < -90) ? 180 : 0);
        }
      }
      // keep going for all layers
      radlay1(n, layer + 1, a, sweep / found, distances);
    }
  }

  
</script>
</head>
<body onload="init()">
<div id="sample">
  <h3>GoJS Recentering Radial</h3>
  <div id="myDiagramDiv" style="border: solid 1px black; background: white; width: 100%; height: 600px"></div>
  <label for="maxLayersChanger">Max Layers</label><input type="text" id="maxLayersChanger" name="maxLayers" style="width: 50px"  />
  <button onclick="adjustMaxLayers()">Set Max Layers</button>
  <p>
    Click on a Node to center it and show its relationships.
  </p>
  <p>
    You can set some parameters in the JavaScript code to control how many layers to show,
    whether to draw the circles, and whether to rotate the text.
    It is also easy to add more information to each node, including pictures,
    or to put such information into <a href="../intro/toolTips.html" target="_blank">Tooltips</a>.
  </p>
</div>
</body>
</html>